/*
 * Copyright (c) 2018-2024, Andreas Kling <andreas@ladybird.org>
 * Copyright (c) 2021-2025, Sam Atkins <sam@ladybird.org>
 *
 * SPDX-License-Identifier: BSD-2-Clause
 */

#include <LibWeb/CSS/ComputedProperties.h>
#include <LibWeb/CSS/Keyword.h>
#include <LibWeb/CSS/Parser/Parser.h>
#include <LibWeb/CSS/SelectorEngine.h>
#include <LibWeb/DOM/Attr.h>
#include <LibWeb/DOM/Document.h>
#include <LibWeb/DOM/Element.h>
#include <LibWeb/DOM/NamedNodeMap.h>
#include <LibWeb/DOM/Text.h>
#include <LibWeb/HTML/AttributeNames.h>
#include <LibWeb/HTML/CustomElements/CustomStateSet.h>
#include <LibWeb/HTML/HTMLAnchorElement.h>
#include <LibWeb/HTML/HTMLDetailsElement.h>
#include <LibWeb/HTML/HTMLDialogElement.h>
#include <LibWeb/HTML/HTMLFieldSetElement.h>
#include <LibWeb/HTML/HTMLFormElement.h>
#include <LibWeb/HTML/HTMLHtmlElement.h>
#include <LibWeb/HTML/HTMLInputElement.h>
#include <LibWeb/HTML/HTMLMediaElement.h>
#include <LibWeb/HTML/HTMLMeterElement.h>
#include <LibWeb/HTML/HTMLProgressElement.h>
#include <LibWeb/HTML/HTMLSelectElement.h>
#include <LibWeb/HTML/HTMLTextAreaElement.h>
#include <LibWeb/Infra/Strings.h>
#include <LibWeb/Namespace.h>
#include <LibWeb/SVG/SVGAElement.h>

namespace Web::SelectorEngine {

static inline bool matches(CSS::Selector const& selector, int component_list_index, DOM::Element const& element, GC::Ptr<DOM::Element const> shadow_host, MatchContext& context, GC::Ptr<DOM::ParentNode const> scope, SelectorKind selector_kind, GC::Ptr<DOM::Element const> anchor = nullptr);

// Upward traversal for descendant (' ') and immediate child combinator ('>')
// If we're starting inside a shadow tree, traversal stops at the nearest shadow host.
// This is an implementation detail of the :host selector. Otherwise we would just traverse up to the document root.
static inline GC::Ptr<DOM::Node const> traverse_up(GC::Ptr<DOM::Node const> node, GC::Ptr<DOM::Element const> shadow_host)
{
    if (!node)
        return nullptr;

    if (shadow_host) {
        // NOTE: We only traverse up to the shadow host, not beyond.
        if (node == shadow_host)
            return nullptr;

        return node->parent_or_shadow_host_element();
    }
    return node->parent();
}

// https://www.rfc-editor.org/rfc/rfc4647.html#section-3.3.2
// NB: Language tags only use ASCII characters, so we can get away with using StringView.
static bool language_range_matches_tag(StringView language_range, StringView language_tag)
{
    // 1. Split both the extended language range and the language tag being compared into a list of subtags by
    //    dividing on the hyphen (%x2D) character.
    auto range_subtags = language_range.split_view('-', SplitBehavior::KeepEmpty);
    auto tag_subtags = language_tag.split_view('-', SplitBehavior::KeepEmpty);

    //    Two subtags match if either they are the same when compared case-insensitively or the language range's subtag
    //    is the wildcard '*'.
    auto subtags_match = [](StringView language_range_subtag, StringView language_subtag) {
        return language_range_subtag == "*"sv
            || language_range_subtag.equals_ignoring_ascii_case(language_subtag);
    };

    // 2. Begin with the first subtag in each list. If the first subtag in the range does not match the first
    //    subtag in the tag, the overall match fails. Otherwise, move to the next subtag in both the range and the
    //    tag.
    auto tag_subtag = tag_subtags.begin();
    auto range_subtag = range_subtags.begin();
    if (!subtags_match(*range_subtag, *tag_subtag))
        return false;
    ++tag_subtag;
    ++range_subtag;

    // 3. While there are more subtags left in the language range's list:
    while (!range_subtag.is_end()) {
        // A. If the subtag currently being examined in the range is the wildcard ('*'), move to the next subtag in
        //    the range and continue with the loop.
        if (*range_subtag == "*"sv) {
            ++range_subtag;
            continue;
        }

        // B. Else, if there are no more subtags in the language tag's list, the match fails.
        if (tag_subtag.is_end())
            return false;

        // C. Else, if the current subtag in the range's list matches the current subtag in the language tag's
        //    list, move to the next subtag in both lists and continue with the loop.
        if (subtags_match(*range_subtag, *tag_subtag)) {
            ++range_subtag;
            ++tag_subtag;
            continue;
        }

        // D. Else, if the language tag's subtag is a "singleton" (a single letter or digit, which includes the
        //    private-use subtag 'x') the match fails.
        if (tag_subtag->length() == 1 && is_ascii_alphanumeric((*tag_subtag)[0])) {
            return false;
        }

        // E. Else, move to the next subtag in the language tag's list and continue with the loop.
        ++tag_subtag;
    }

    // 4. When the language range's list has no more subtags, the match succeeds.
    return true;
}

// https://drafts.csswg.org/selectors-4/#the-lang-pseudo
static inline bool matches_lang_pseudo_class(DOM::Element const& element, Vector<FlyString> const& languages)
{
    auto maybe_element_language = element.lang();
    if (!maybe_element_language.has_value())
        return false;

    auto element_language = maybe_element_language.release_value();

    // The element’s content language matches a language range if its content language, as represented in BCP 47
    // syntax, matches the given language range in an extended filtering operation per [RFC4647] Matching of Language
    // Tags (section 3.3.2). Both the content language and the language range must be canonicalized and converted to
    // extlang form as per section 4.5 of [RFC5646] prior to the extended filtering operation. The matching is
    // performed case-insensitively within the ASCII range.

    // FIXME: Canonicalize both as extlang.

    for (auto const& language_range : languages) {
        if (language_range_matches_tag(language_range, element_language))
            return true;
    }
    return false;
}

// https://drafts.csswg.org/selectors-4/#relational
static inline bool matches_relative_selector(CSS::Selector const& selector, size_t compound_index, DOM::Element const& element, GC::Ptr<DOM::Element const> shadow_host, MatchContext& context, GC::Ref<DOM::Element const> anchor)
{
    if (compound_index >= selector.compound_selectors().size())
        return matches(selector, element, shadow_host, context, {}, {}, SelectorKind::Relative, anchor);

    switch (selector.compound_selectors()[compound_index].combinator) {
    // Shouldn't be possible because we've parsed relative selectors, which always have a combinator, implicitly or explicitly.
    case CSS::Selector::Combinator::None:
        VERIFY_NOT_REACHED();
    case CSS::Selector::Combinator::Descendant: {
        bool has = false;
        element.for_each_in_subtree([&](auto const& descendant) {
            if (!descendant.is_element())
                return TraversalDecision::Continue;
            auto const& descendant_element = static_cast<DOM::Element const&>(descendant);
            if (matches(selector, descendant_element, shadow_host, context, {}, {}, SelectorKind::Relative, anchor)) {
                has = true;
                return TraversalDecision::Break;
            }
            return TraversalDecision::Continue;
        });
        return has;
    }
    case CSS::Selector::Combinator::ImmediateChild: {
        bool has = false;
        element.for_each_child([&](DOM::Node const& child) {
            if (!child.is_element())
                return IterationDecision::Continue;
            auto const& child_element = static_cast<DOM::Element const&>(child);
            if (!matches(selector, compound_index, child_element, shadow_host, context, {}, SelectorKind::Relative, anchor))
                return IterationDecision::Continue;
            if (matches_relative_selector(selector, compound_index + 1, child_element, shadow_host, context, anchor)) {
                has = true;
                return IterationDecision::Break;
            }
            return IterationDecision::Continue;
        });
        return has;
    }
    case CSS::Selector::Combinator::NextSibling: {
        if (context.collect_per_element_selector_involvement_metadata) {
            const_cast<DOM::Element&>(*anchor).set_affected_by_has_pseudo_class_with_relative_selector_that_has_sibling_combinator(true);
        }
        auto* sibling = element.next_element_sibling();
        if (!sibling)
            return false;
        if (!matches(selector, compound_index, *sibling, shadow_host, context, {}, SelectorKind::Relative, anchor))
            return false;
        return matches_relative_selector(selector, compound_index + 1, *sibling, shadow_host, context, anchor);
    }
    case CSS::Selector::Combinator::SubsequentSibling: {
        if (context.collect_per_element_selector_involvement_metadata) {
            const_cast<DOM::Element&>(*anchor).set_affected_by_has_pseudo_class_with_relative_selector_that_has_sibling_combinator(true);
        }
        for (auto const* sibling = element.next_element_sibling(); sibling; sibling = sibling->next_element_sibling()) {
            if (!matches(selector, compound_index, *sibling, shadow_host, context, {}, SelectorKind::Relative, anchor))
                continue;
            if (matches_relative_selector(selector, compound_index + 1, *sibling, shadow_host, context, anchor))
                return true;
        }
        return false;
    }
    case CSS::Selector::Combinator::Column:
        TODO();
    }
    return false;
}

// https://drafts.csswg.org/selectors-4/#relational
static inline bool matches_has_pseudo_class(CSS::Selector const& selector, DOM::Element const& anchor, GC::Ptr<DOM::Element const> shadow_host, MatchContext& context)
{
    return matches_relative_selector(selector, 0, anchor, shadow_host, context, anchor);
}

static bool matches_hover_pseudo_class(DOM::Element const& element)
{
    auto* hovered_node = element.document().hovered_node();
    if (!hovered_node)
        return false;
    if (&element == hovered_node)
        return true;
    return element.is_shadow_including_ancestor_of(*hovered_node);
}

// https://html.spec.whatwg.org/multipage/semantics-other.html#selector-indeterminate
static inline bool matches_indeterminate_pseudo_class(DOM::Element const& element)
{
    // The :indeterminate pseudo-class must match any element falling into one of the following categories:
    // - input elements whose type attribute is in the Checkbox state and whose indeterminate IDL attribute is set to true
    // FIXME: - input elements whose type attribute is in the Radio Button state and whose radio button group contains no input elements whose checkedness state is true.
    if (is<HTML::HTMLInputElement>(element)) {
        auto const& input_element = static_cast<HTML::HTMLInputElement const&>(element);
        switch (input_element.type_state()) {
        case HTML::HTMLInputElement::TypeAttributeState::Checkbox:
            // https://whatpr.org/html-attr-input-switch/9546/semantics-other.html#selector-indeterminate
            // input elements whose type attribute is in the Checkbox state, whose switch attribute is not set
            return input_element.indeterminate() && !element.has_attribute(HTML::AttributeNames::switch_);
        default:
            return false;
        }
    }
    // - progress elements with no value content attribute
    if (is<HTML::HTMLProgressElement>(element)) {
        return !element.has_attribute(HTML::AttributeNames::value);
    }
    return false;
}

static inline void for_each_matching_attribute(CSS::Selector::SimpleSelector::Attribute const& attribute_selector, GC::Ptr<CSS::CSSStyleSheet const> style_sheet_for_rule, DOM::Element const& element, Function<IterationDecision(DOM::Attr const&)> const& process_attribute)
{
    auto const& qualified_name = attribute_selector.qualified_name;
    auto const& attribute_name = qualified_name.name.name;

    switch (qualified_name.namespace_type) {
    // "In keeping with the Namespaces in the XML recommendation, default namespaces do not apply to attributes,
    //  therefore attribute selectors without a namespace component apply only to attributes that have no namespace (equivalent to "|attr")"
    case CSS::Selector::SimpleSelector::QualifiedName::NamespaceType::Default:
    case CSS::Selector::SimpleSelector::QualifiedName::NamespaceType::None:
        if (auto const* attribute = element.attributes()->get_attribute(attribute_name))
            (void)process_attribute(*attribute);
        return;
    case CSS::Selector::SimpleSelector::QualifiedName::NamespaceType::Any: {
        // When comparing the name part of a CSS attribute selector to the names of attributes on HTML elements in HTML
        // documents, the name part of the CSS attribute selector must first be converted to ASCII lowercase. The same
        // selector when compared to other attributes must be compared according to its original case. In both cases, the
        // comparison is case-sensitive.
        // https://html.spec.whatwg.org/multipage/semantics-other.html#case-sensitivity-of-selectors
        bool const case_insensitive = element.document().is_html_document() && element.namespace_uri() == Namespace::HTML;

        for (auto i = 0u; i < element.attributes()->length(); ++i) {
            auto const* attr = element.attributes()->item(i);
            bool matches = case_insensitive
                ? attr->local_name().equals_ignoring_ascii_case(attribute_name)
                : attr->local_name() == attribute_name;
            if (matches) {
                if (process_attribute(*attr) == IterationDecision::Break)
                    break;
            }
        }
        return;
    }
    case CSS::Selector::SimpleSelector::QualifiedName::NamespaceType::Named:
        if (!style_sheet_for_rule)
            return;
        auto const& selector_namespace = style_sheet_for_rule->namespace_uri(qualified_name.namespace_);
        if (!selector_namespace.has_value())
            return;

        if (auto const* attribute = element.attributes()->get_attribute_ns(selector_namespace, attribute_name))
            (void)process_attribute(*attribute);
        return;
    }
    VERIFY_NOT_REACHED();
}

static bool matches_single_attribute(CSS::Selector::SimpleSelector::Attribute const& attribute_selector, DOM::Attr const& attribute, CaseSensitivity case_sensitivity)
{
    auto const case_insensitive_match = case_sensitivity == CaseSensitivity::CaseInsensitive;

    switch (attribute_selector.match_type) {
    case CSS::Selector::SimpleSelector::Attribute::MatchType::ExactValueMatch:
        return case_insensitive_match
            ? attribute.value().equals_ignoring_ascii_case(attribute_selector.value)
            : attribute.value() == attribute_selector.value;
    case CSS::Selector::SimpleSelector::Attribute::MatchType::ContainsWord: {
        if (attribute_selector.value.is_empty()) {
            // This selector is always false is match value is empty.
            return false;
        }
        auto const& attribute_value = attribute.value();
        auto const view = attribute_value.bytes_as_string_view().split_view(' ');
        auto const size = view.size();
        for (size_t i = 0; i < size; ++i) {
            auto const value = view.at(i);
            if (case_insensitive_match
                    ? value.equals_ignoring_ascii_case(attribute_selector.value)
                    : value == attribute_selector.value) {
                return true;
            }
        }
        return false;
    }
    case CSS::Selector::SimpleSelector::Attribute::MatchType::ContainsString:
        return !attribute_selector.value.is_empty()
            && attribute.value().contains(attribute_selector.value, case_sensitivity);
    case CSS::Selector::SimpleSelector::Attribute::MatchType::StartsWithSegment: {
        // https://www.w3.org/TR/CSS2/selector.html#attribute-selectors
        // [att|=val]
        // Represents an element with the att attribute, its value either being exactly "val" or beginning with "val" immediately followed by "-" (U+002D).

        auto const& element_attr_value = attribute.value();
        if (element_attr_value.is_empty()) {
            // If the attribute value on element is empty, the selector is true
            // if the match value is also empty and false otherwise.
            return attribute_selector.value.is_empty();
        }
        if (attribute_selector.value.is_empty()) {
            return false;
        }

        auto element_attribute_length = element_attr_value.bytes_as_string_view().length();
        auto attribute_length = attribute_selector.value.bytes_as_string_view().length();
        if (element_attribute_length < attribute_length)
            return false;

        if (attribute_length == element_attribute_length) {
            return case_insensitive_match
                ? element_attr_value.equals_ignoring_ascii_case(attribute_selector.value)
                : element_attr_value == attribute_selector.value;
        }

        return element_attr_value.starts_with_bytes(attribute_selector.value, case_insensitive_match ? CaseSensitivity::CaseInsensitive : CaseSensitivity::CaseSensitive) && element_attr_value.bytes_as_string_view()[attribute_length] == '-';
    }
    case CSS::Selector::SimpleSelector::Attribute::MatchType::StartsWithString:
        return !attribute_selector.value.is_empty()
            && attribute.value().bytes_as_string_view().starts_with(attribute_selector.value, case_sensitivity);
    case CSS::Selector::SimpleSelector::Attribute::MatchType::EndsWithString:
        return !attribute_selector.value.is_empty()
            && attribute.value().bytes_as_string_view().ends_with(attribute_selector.value, case_sensitivity);
    case CSS::Selector::SimpleSelector::Attribute::MatchType::HasAttribute:
        return true;
    }
    return false;
}

static inline bool matches_attribute(CSS::Selector::SimpleSelector::Attribute const& attribute, [[maybe_unused]] GC::Ptr<CSS::CSSStyleSheet const> style_sheet_for_rule, DOM::Element const& element)
{
    auto const& attribute_name = attribute.qualified_name.name.name;

    auto case_sensitivity = [&](CSS::Selector::SimpleSelector::Attribute::CaseType case_type) {
        switch (case_type) {
        case CSS::Selector::SimpleSelector::Attribute::CaseType::CaseInsensitiveMatch:
            return CaseSensitivity::CaseInsensitive;
        case CSS::Selector::SimpleSelector::Attribute::CaseType::CaseSensitiveMatch:
            return CaseSensitivity::CaseSensitive;
        case CSS::Selector::SimpleSelector::Attribute::CaseType::DefaultMatch:
            // See: https://html.spec.whatwg.org/multipage/semantics-other.html#case-sensitivity-of-selectors
            if (element.document().is_html_document()
                && element.namespace_uri() == Namespace::HTML
                && attribute.qualified_name.namespace_type == CSS::Selector::SimpleSelector::QualifiedName::NamespaceType::Default
                && attribute_name.is_one_of(
                    HTML::AttributeNames::accept, HTML::AttributeNames::accept_charset, HTML::AttributeNames::align,
                    HTML::AttributeNames::alink, HTML::AttributeNames::axis, HTML::AttributeNames::bgcolor, HTML::AttributeNames::charset,
                    HTML::AttributeNames::checked, HTML::AttributeNames::clear, HTML::AttributeNames::codetype, HTML::AttributeNames::color,
                    HTML::AttributeNames::compact, HTML::AttributeNames::declare, HTML::AttributeNames::defer, HTML::AttributeNames::dir,
                    HTML::AttributeNames::direction, HTML::AttributeNames::disabled, HTML::AttributeNames::enctype, HTML::AttributeNames::face,
                    HTML::AttributeNames::frame, HTML::AttributeNames::hreflang, HTML::AttributeNames::http_equiv, HTML::AttributeNames::lang,
                    HTML::AttributeNames::language, HTML::AttributeNames::link, HTML::AttributeNames::media, HTML::AttributeNames::method,
                    HTML::AttributeNames::multiple, HTML::AttributeNames::nohref, HTML::AttributeNames::noresize, HTML::AttributeNames::noshade,
                    HTML::AttributeNames::nowrap, HTML::AttributeNames::readonly, HTML::AttributeNames::rel, HTML::AttributeNames::rev,
                    HTML::AttributeNames::rules, HTML::AttributeNames::scope, HTML::AttributeNames::scrolling, HTML::AttributeNames::selected,
                    HTML::AttributeNames::shape, HTML::AttributeNames::target, HTML::AttributeNames::text, HTML::AttributeNames::type,
                    HTML::AttributeNames::valign, HTML::AttributeNames::valuetype, HTML::AttributeNames::vlink)) {
                return CaseSensitivity::CaseInsensitive;
            }

            return CaseSensitivity::CaseSensitive;
        }
        VERIFY_NOT_REACHED();
    }(attribute.case_type);

    bool found_matching_attribute = false;
    for_each_matching_attribute(attribute, style_sheet_for_rule, element, [&attribute, case_sensitivity, &found_matching_attribute](DOM::Attr const& attr) {
        if (matches_single_attribute(attribute, attr, case_sensitivity)) {
            found_matching_attribute = true;
            return IterationDecision::Break;
        }
        return IterationDecision::Continue;
    });

    return found_matching_attribute;
}

static inline DOM::Element const* previous_sibling_with_same_tag_name(DOM::Element const& element)
{
    for (auto const* sibling = element.previous_element_sibling(); sibling; sibling = sibling->previous_element_sibling()) {
        if (sibling->tag_name() == element.tag_name())
            return sibling;
    }
    return nullptr;
}

static inline DOM::Element const* next_sibling_with_same_tag_name(DOM::Element const& element)
{
    for (auto const* sibling = element.next_element_sibling(); sibling; sibling = sibling->next_element_sibling()) {
        if (sibling->tag_name() == element.tag_name())
            return sibling;
    }
    return nullptr;
}

/// Returns true if this selector should be blocked from matching against the shadow host from within a shadow tree.
/// Only :host pseudo-class is allowed to match the shadow host from within shadow tree, all other selectors (including other pseudo-classes) are blocked.
/// Compound selectors (:has(), :is(), :where()), nesting, and pseudo-elements are allowed as they may contain or be contained within :host.
static inline bool should_block_shadow_host_matching(CSS::Selector::SimpleSelector const& component, GC::Ptr<DOM::Element const> shadow_host, DOM::Element const& element)
{
    if (!shadow_host || &element != shadow_host.ptr())
        return false;

    // From within shadow tree, only :host pseudo-class can match the host element
    if (component.type == CSS::Selector::SimpleSelector::Type::PseudoClass) {
        auto const& pseudo_class = component.pseudo_class();
        return pseudo_class.type != CSS::PseudoClass::Host
            && pseudo_class.type != CSS::PseudoClass::Has
            && pseudo_class.type != CSS::PseudoClass::Is
            && pseudo_class.type != CSS::PseudoClass::Where;
    }

    // Allow nesting and PseudoElement as it may contain :host class
    if (component.type == CSS::Selector::SimpleSelector::Type::Nesting || component.type == CSS::Selector::SimpleSelector::Type::PseudoElement)
        return false;

    return true;
}
// https://html.spec.whatwg.org/multipage/semantics-other.html#selector-read-write
static bool matches_read_write_pseudo_class(DOM::Element const& element)
{
    // The :read-write pseudo-class must match any element falling into one of the following categories,
    // which for the purposes of Selectors are thus considered user-alterable: [SELECTORS]
    // - input elements to which the readonly attribute applies, and that are mutable
    //   (i.e. that do not have the readonly attribute specified and that are not disabled)
    if (auto const* input_element = as_if<HTML::HTMLInputElement>(element))
        return input_element->is_allowed_to_be_readonly()
            && !input_element->has_attribute(HTML::AttributeNames::readonly) && input_element->enabled();
    // - textarea elements that do not have a readonly attribute, and that are not disabled
    if (auto const* input_element = as_if<HTML::HTMLTextAreaElement>(element))
        return !input_element->has_attribute(HTML::AttributeNames::readonly) && input_element->enabled();
    // - elements that are editing hosts or editable and are neither input elements nor textarea elements
    return element.is_editable_or_editing_host();
}

// https://drafts.csswg.org/selectors-4/#open-state
static bool matches_open_state_pseudo_class(DOM::Element const& element, bool open)
{
    // The :open pseudo-class represents an element that has both “open” and “closed” states,
    // and which is currently in the “open” state.

    // https://html.spec.whatwg.org/multipage/semantics-other.html#selector-open
    // The :open pseudo-class must match any element falling into one of the following categories:
    // - details elements that have an open attribute
    // - dialog elements that have an open attribute
    if (is<HTML::HTMLDetailsElement>(element) || is<HTML::HTMLDialogElement>(element))
        return open == element.has_attribute(HTML::AttributeNames::open);
    // - select elements that are a drop-down box and whose drop-down boxes are open
    if (auto const* select = as_if<HTML::HTMLSelectElement>(element))
        return open == select->is_open();
    // - input elements that support a picker and whose pickers are open
    if (auto const* input = as_if<HTML::HTMLInputElement>(element))
        return open == (input->supports_a_picker() && input->is_open());

    return false;
}

// https://drafts.csswg.org/css-scoping/#host-selector
static inline bool matches_host_pseudo_class(GC::Ref<DOM::Element const> element, GC::Ptr<DOM::Element const> shadow_host, MatchContext& context, CSS::SelectorList const& argument_selector_list)
{
    // When evaluated in the context of a shadow tree, it matches the shadow tree’s shadow host if the shadow host,
    // in its normal context, matches the selector argument. In any other context, it matches nothing.
    if (!shadow_host || element != shadow_host)
        return false;

    // NOTE: There's either 0 or 1 argument selector, since the syntax is :host or :host(<compound-selector>)
    if (!argument_selector_list.is_empty())
        return matches(argument_selector_list.first(), element, nullptr, context);

    return true;
}

static bool matches_optimal_value_pseudo_class(DOM::Element const& element, HTML::HTMLMeterElement::ValueState desired_state)
{
    if (auto* meter = as_if<HTML::HTMLMeterElement>(element))
        return meter->value_state() == desired_state;
    return false;
}

static inline bool matches_pseudo_class(CSS::Selector::SimpleSelector::PseudoClassSelector const& pseudo_class, DOM::Element const& element, GC::Ptr<DOM::Element const> shadow_host, MatchContext& context, GC::Ptr<DOM::ParentNode const> scope, SelectorKind selector_kind)
{
    context.attempted_pseudo_class_matches.set(pseudo_class.type, true);
    switch (pseudo_class.type) {
    case CSS::PseudoClass::__Count:
        VERIFY_NOT_REACHED();
    case CSS::PseudoClass::Link:
    case CSS::PseudoClass::AnyLink:
        // NOTE: AnyLink should match whether the link is visited or not, so if we ever start matching
        //       :visited, we'll need to handle these differently.
        return element.matches_link_pseudo_class();
    case CSS::PseudoClass::LocalLink: {
        return element.matches_local_link_pseudo_class();
    }
    case CSS::PseudoClass::Visited:
        // FIXME: Maybe match this selector sometimes?
        return false;
    case CSS::PseudoClass::Active:
        return element.is_active();
    case CSS::PseudoClass::Hover:
        return matches_hover_pseudo_class(element);
    case CSS::PseudoClass::Focus:
        return element.is_focused();
    case CSS::PseudoClass::FocusVisible:
        return element.is_focused() && element.should_indicate_focus();
    case CSS::PseudoClass::FocusWithin: {
        auto focused_area = element.document().focused_area();
        return focused_area && element.is_inclusive_ancestor_of(*focused_area);
    }
    case CSS::PseudoClass::FirstChild:
        if (context.collect_per_element_selector_involvement_metadata) {
            const_cast<DOM::Element&>(element).set_affected_by_sibling_position_or_count_pseudo_class(true);
        }
        return !element.previous_element_sibling();
    case CSS::PseudoClass::LastChild:
        if (context.collect_per_element_selector_involvement_metadata) {
            const_cast<DOM::Element&>(element).set_affected_by_sibling_position_or_count_pseudo_class(true);
        }
        return !element.next_element_sibling();
    case CSS::PseudoClass::OnlyChild:
        if (context.collect_per_element_selector_involvement_metadata) {
            const_cast<DOM::Element&>(element).set_affected_by_sibling_position_or_count_pseudo_class(true);
        }
        return !(element.previous_element_sibling() || element.next_element_sibling());
    case CSS::PseudoClass::Empty: {
        if (!element.has_children())
            return true;
        if (element.first_child_of_type<DOM::Element>())
            return false;
        // NOTE: CSS Selectors level 4 changed ":empty" to also match whitespace-only text nodes.
        //       However, none of the major browser supports this yet, so let's just hang back until they do.
        bool has_nonempty_text_child = false;
        element.for_each_child_of_type<DOM::Text>([&](auto const& text_child) {
            if (!text_child.data().is_empty()) {
                has_nonempty_text_child = true;
                return IterationDecision::Break;
            }
            return IterationDecision::Continue;
        });
        return !has_nonempty_text_child;
    }
    case CSS::PseudoClass::Root:
        return is<HTML::HTMLHtmlElement>(element);
    case CSS::PseudoClass::Host:
        return matches_host_pseudo_class(element, shadow_host, context, pseudo_class.argument_selector_list);
    case CSS::PseudoClass::Scope:
        return scope ? &element == scope : is<HTML::HTMLHtmlElement>(element);
    case CSS::PseudoClass::FirstOfType:
        if (context.collect_per_element_selector_involvement_metadata) {
            const_cast<DOM::Element&>(element).set_affected_by_sibling_position_or_count_pseudo_class(true);
        }
        return !previous_sibling_with_same_tag_name(element);
    case CSS::PseudoClass::LastOfType:
        if (context.collect_per_element_selector_involvement_metadata) {
            const_cast<DOM::Element&>(element).set_affected_by_sibling_position_or_count_pseudo_class(true);
        }
        return !next_sibling_with_same_tag_name(element);
    case CSS::PseudoClass::OnlyOfType:
        if (context.collect_per_element_selector_involvement_metadata) {
            const_cast<DOM::Element&>(element).set_affected_by_sibling_position_or_count_pseudo_class(true);
        }
        return !previous_sibling_with_same_tag_name(element) && !next_sibling_with_same_tag_name(element);
    case CSS::PseudoClass::Lang:
        return matches_lang_pseudo_class(element, pseudo_class.languages);
    case CSS::PseudoClass::Disabled:
        return element.matches_disabled_pseudo_class();
    case CSS::PseudoClass::Enabled:
        return element.matches_enabled_pseudo_class();
    case CSS::PseudoClass::Checked:
        return element.matches_checked_pseudo_class();
    case CSS::PseudoClass::Unchecked:
        return element.matches_unchecked_pseudo_class();
    case CSS::PseudoClass::Indeterminate:
        return matches_indeterminate_pseudo_class(element);
    case CSS::PseudoClass::HighValue:
        if (auto* meter = as_if<HTML::HTMLMeterElement>(element))
            return meter->value() > meter->high();
        return false;
    case CSS::PseudoClass::LowValue:
        if (auto* meter = as_if<HTML::HTMLMeterElement>(element))
            return meter->value() < meter->low();
        return false;
    case CSS::PseudoClass::OptimalValue:
        return matches_optimal_value_pseudo_class(element, HTML::HTMLMeterElement::ValueState::Optimal);
    case CSS::PseudoClass::SuboptimalValue:
        return matches_optimal_value_pseudo_class(element, HTML::HTMLMeterElement::ValueState::Suboptimal);
    case CSS::PseudoClass::EvenLessGoodValue:
        return matches_optimal_value_pseudo_class(element, HTML::HTMLMeterElement::ValueState::EvenLessGood);
    case CSS::PseudoClass::Defined:
        return element.is_defined();
    case CSS::PseudoClass::Has:
        // :has() cannot be nested in a :has()
        if (selector_kind == SelectorKind::Relative)
            return false;
        if (context.collect_per_element_selector_involvement_metadata) {
            if (&element == context.subject) {
                const_cast<DOM::Element&>(element).set_affected_by_has_pseudo_class_in_subject_position(true);
            } else {
                const_cast<DOM::Element&>(element).set_affected_by_has_pseudo_class_in_non_subject_position(true);
            }
        }
        // These selectors should be relative selectors (https://drafts.csswg.org/selectors-4/#relative-selector)
        for (auto& selector : pseudo_class.argument_selector_list) {
            if (matches_has_pseudo_class(selector, element, shadow_host, context))
                return true;
        }
        return false;
    case CSS::PseudoClass::Is:
    case CSS::PseudoClass::Where:
        for (auto& selector : pseudo_class.argument_selector_list) {
            if (matches(selector, element, shadow_host, context))
                return true;
        }
        return false;
    case CSS::PseudoClass::Not:
        for (auto& selector : pseudo_class.argument_selector_list) {
            if (matches(selector, element, shadow_host, context))
                return false;
        }
        return true;
    case CSS::PseudoClass::NthChild:
    case CSS::PseudoClass::NthLastChild:
    case CSS::PseudoClass::NthOfType:
    case CSS::PseudoClass::NthLastOfType: {
        auto const* parent = element.parent();
        if (!parent)
            return false;

        if (context.collect_per_element_selector_involvement_metadata) {
            const_cast<DOM::Element&>(element).set_affected_by_nth_child_pseudo_class(true);
        }

        auto matches_selector_list = [&context, shadow_host](CSS::SelectorList const& list, DOM::Element const& element) {
            if (list.is_empty())
                return true;
            for (auto const& child_selector : list) {
                if (matches(child_selector, element, shadow_host, context)) {
                    return true;
                }
            }
            return false;
        };

        int index = 1;
        switch (pseudo_class.type) {
        case CSS::PseudoClass::__Count:
            VERIFY_NOT_REACHED();
        case CSS::PseudoClass::NthChild: {
            if (!matches_selector_list(pseudo_class.argument_selector_list, element))
                return false;
            for (auto* child = parent->first_child_of_type<DOM::Element>(); child && child != &element; child = child->next_element_sibling()) {
                if (matches_selector_list(pseudo_class.argument_selector_list, *child))
                    ++index;
            }
            break;
        }
        case CSS::PseudoClass::NthLastChild: {
            if (!matches_selector_list(pseudo_class.argument_selector_list, element))
                return false;
            for (auto* child = parent->last_child_of_type<DOM::Element>(); child && child != &element; child = child->previous_element_sibling()) {
                if (matches_selector_list(pseudo_class.argument_selector_list, *child))
                    ++index;
            }
            break;
        }
        case CSS::PseudoClass::NthOfType: {
            for (auto* child = previous_sibling_with_same_tag_name(element); child; child = previous_sibling_with_same_tag_name(*child))
                ++index;
            break;
        }
        case CSS::PseudoClass::NthLastOfType: {
            for (auto* child = next_sibling_with_same_tag_name(element); child; child = next_sibling_with_same_tag_name(*child))
                ++index;
            break;
        }
        default:
            VERIFY_NOT_REACHED();
        }
        return pseudo_class.an_plus_b_pattern.matches(index);
    }
    case CSS::PseudoClass::Playing: {
        if (!is<HTML::HTMLMediaElement>(element))
            return false;
        auto const& media_element = static_cast<HTML::HTMLMediaElement const&>(element);
        return !media_element.paused();
    }
    case CSS::PseudoClass::Paused: {
        if (!is<HTML::HTMLMediaElement>(element))
            return false;
        auto const& media_element = static_cast<HTML::HTMLMediaElement const&>(element);
        return media_element.paused();
    }
    case CSS::PseudoClass::Seeking: {
        if (!is<HTML::HTMLMediaElement>(element))
            return false;
        auto const& media_element = static_cast<HTML::HTMLMediaElement const&>(element);
        return media_element.seeking();
    }
    case CSS::PseudoClass::Muted: {
        if (!is<HTML::HTMLMediaElement>(element))
            return false;
        auto const& media_element = static_cast<HTML::HTMLMediaElement const&>(element);
        return media_element.muted();
    }
    case CSS::PseudoClass::VolumeLocked: {
        // FIXME: Currently we don't allow the user to specify an override volume, so this is always false.
        //        Once we do, implement this!
        return false;
    }
    case CSS::PseudoClass::Buffering: {
        if (!is<HTML::HTMLMediaElement>(element))
            return false;
        auto const& media_element = static_cast<HTML::HTMLMediaElement const&>(element);
        return media_element.blocked();
    }
    case CSS::PseudoClass::Stalled: {
        if (!is<HTML::HTMLMediaElement>(element))
            return false;
        auto const& media_element = static_cast<HTML::HTMLMediaElement const&>(element);
        return media_element.stalled();
    }
    case CSS::PseudoClass::Target:
        return element.is_target();
    case CSS::PseudoClass::Dir: {
        // "Values other than ltr and rtl are not invalid, but do not match anything."
        // - https://www.w3.org/TR/selectors-4/#the-dir-pseudo
        if (!pseudo_class.ident.has_value())
            return false;
        if (!first_is_one_of(pseudo_class.ident->keyword, CSS::Keyword::Ltr, CSS::Keyword::Rtl))
            return false;
        switch (element.directionality()) {
        case DOM::Element::Directionality::Ltr:
            return pseudo_class.ident->keyword == CSS::Keyword::Ltr;
        case DOM::Element::Directionality::Rtl:
            return pseudo_class.ident->keyword == CSS::Keyword::Rtl;
        }
        VERIFY_NOT_REACHED();
    }
    case CSS::PseudoClass::ReadOnly:
        return !matches_read_write_pseudo_class(element);
    case CSS::PseudoClass::ReadWrite:
        return matches_read_write_pseudo_class(element);
    case CSS::PseudoClass::PlaceholderShown: {
        // https://html.spec.whatwg.org/multipage/semantics-other.html#selector-placeholder-shown
        //  The :placeholder-shown pseudo-class must match any element falling into one of the following categories:
        // - input elements that have a placeholder attribute whose value is currently being presented to the user.
        if (is<HTML::HTMLInputElement>(element) && element.has_attribute(HTML::AttributeNames::placeholder)) {
            auto const& input_element = static_cast<HTML::HTMLInputElement const&>(element);
            return input_element.placeholder_element() && input_element.placeholder_value().has_value();
        }
        // - FIXME: textarea elements that have a placeholder attribute whose value is currently being presented to the user.
        return false;
    }
    case CSS::PseudoClass::Open:
        return matches_open_state_pseudo_class(element, pseudo_class.type == CSS::PseudoClass::Open);
    case CSS::PseudoClass::Modal: {
        // https://drafts.csswg.org/selectors/#modal-state
        if (is<HTML::HTMLDialogElement>(element)) {
            auto const& dialog_element = static_cast<HTML::HTMLDialogElement const&>(element);
            return dialog_element.is_modal();
        }
        // FIXME: fullscreen elements are also modal.
        return false;
    }
    case CSS::PseudoClass::PopoverOpen: {
        // https://html.spec.whatwg.org/multipage/semantics-other.html#selector-popover-open
        // The :popover-open pseudo-class is defined to match any HTML element whose popover attribute is not in the
        // No Popover state and whose popover visibility state is showing.
        if (is<HTML::HTMLElement>(element) && element.has_attribute(HTML::AttributeNames::popover)) {
            auto& html_element = static_cast<HTML::HTMLElement const&>(element);
            return html_element.popover_visibility_state() == HTML::HTMLElement::PopoverVisibilityState::Showing;
        }

        return false;
    }
    case CSS::PseudoClass::Valid: {
        // https://html.spec.whatwg.org/multipage/semantics-other.html#selector-valid
        // The :valid pseudo-class must match any element falling into one of the following categories:

        // - elements that are candidates for constraint validation and that satisfy their constraints
        if (auto form_associated_element = as_if<Web::HTML::FormAssociatedElement>(element))
            if (form_associated_element->is_candidate_for_constraint_validation() && form_associated_element->satisfies_its_constraints())
                return true;

        // - form elements that are not the form owner of any elements that themselves are candidates for constraint validation but do not satisfy their constraints
        if (auto form_element = as_if<Web::HTML::HTMLFormElement>(element)) {
            bool has_invalid_elements = false;
            element.for_each_in_subtree([&](auto& node) {
                if (auto form_associated_element = as_if<Web::HTML::FormAssociatedElement>(&node)) {
                    if (form_associated_element->form() == form_element && form_associated_element->is_candidate_for_constraint_validation() && !form_associated_element->satisfies_its_constraints()) {
                        has_invalid_elements = true;
                        return TraversalDecision::Break;
                    }
                }
                return TraversalDecision::Continue;
            });
            if (!has_invalid_elements)
                return true;
        }

        // - fieldset elements that have no descendant elements that themselves are candidates for constraint validation but do not satisfy their constraints
        if (is<Web::HTML::HTMLFieldSetElement>(element)) {
            bool has_invalid_children = false;
            element.for_each_in_subtree([&](auto& node) {
                if (auto form_associated_element = as_if<Web::HTML::FormAssociatedElement>(&node)) {
                    if (form_associated_element->is_candidate_for_constraint_validation() && !form_associated_element->satisfies_its_constraints()) {
                        has_invalid_children = true;
                        return TraversalDecision::Break;
                    }
                }
                return TraversalDecision::Continue;
            });
            if (!has_invalid_children)
                return true;
        }

        return false;
    }
    case CSS::PseudoClass::Invalid: {
        // https://html.spec.whatwg.org/multipage/semantics-other.html#selector-invalid
        // The :invalid pseudo-class must match any element falling into one of the following categories:

        // - elements that are candidates for constraint validation but that do not satisfy their constraints
        if (auto form_associated_element = as_if<Web::HTML::FormAssociatedElement>(element))
            if (form_associated_element->is_candidate_for_constraint_validation() && !form_associated_element->satisfies_its_constraints())
                return true;

        // - form elements that are the form owner of one or more elements that themselves are candidates for constraint validation but do not satisfy their constraints
        if (auto form_element = as_if<Web::HTML::HTMLFormElement>(element)) {
            bool has_invalid_elements = false;
            element.for_each_in_subtree([&](auto& node) {
                if (auto form_associated_element = as_if<Web::HTML::FormAssociatedElement>(&node)) {
                    if (form_associated_element->form() == form_element && form_associated_element->is_candidate_for_constraint_validation() && !form_associated_element->satisfies_its_constraints()) {
                        has_invalid_elements = true;
                        return TraversalDecision::Break;
                    }
                }
                return TraversalDecision::Continue;
            });
            if (has_invalid_elements)
                return true;
        }

        // - fieldset elements that have of one or more descendant elements that themselves are candidates for constraint validation but do not satisfy their constraints
        if (is<Web::HTML::HTMLFieldSetElement>(element)) {
            bool has_invalid_children = false;
            element.for_each_in_subtree([&](auto& node) {
                if (auto form_associated_element = as_if<Web::HTML::FormAssociatedElement>(&node)) {
                    if (form_associated_element->is_candidate_for_constraint_validation() && !form_associated_element->satisfies_its_constraints()) {
                        has_invalid_children = true;
                        return TraversalDecision::Break;
                    }
                }
                return TraversalDecision::Continue;
            });
            if (has_invalid_children)
                return true;
        }

        return false;
    }
    case CSS::PseudoClass::UserValid: {
        // https://html.spec.whatwg.org/multipage/semantics-other.html#selector-user-valid
        // The :user-valid pseudo-class must match input, textarea, and select elements whose user validity is true,
        bool user_validity = false;
        if (auto input_element = as_if<Web::HTML::HTMLInputElement>(element)) {
            user_validity = input_element->user_validity();
        } else if (auto select_element = as_if<Web::HTML::HTMLSelectElement>(element)) {
            user_validity = select_element->user_validity();
        } else if (auto text_area_element = as_if<Web::HTML::HTMLTextAreaElement>(element)) {
            user_validity = text_area_element->user_validity();
        }
        if (!user_validity)
            return false;

        // are candidates for constraint validation, and that satisfy their constraints.
        auto& form_associated_element = as<Web::HTML::FormAssociatedElement>(element);
        if (form_associated_element.is_candidate_for_constraint_validation() && form_associated_element.satisfies_its_constraints())
            return true;

        return false;
    }
    case CSS::PseudoClass::UserInvalid: {
        // https://html.spec.whatwg.org/multipage/semantics-other.html#selector-user-invalid
        // The :user-invalid pseudo-class must match input, textarea, and select elements whose user validity is true,
        bool user_validity = false;
        if (auto input_element = as_if<Web::HTML::HTMLInputElement>(element)) {
            user_validity = input_element->user_validity();
        } else if (auto select_element = as_if<Web::HTML::HTMLSelectElement>(element)) {
            user_validity = select_element->user_validity();
        } else if (auto text_area_element = as_if<Web::HTML::HTMLTextAreaElement>(element)) {
            user_validity = text_area_element->user_validity();
        }
        if (!user_validity)
            return false;

        // are candidates for constraint validation but do not satisfy their constraints.
        auto& form_associated_element = as<Web::HTML::FormAssociatedElement>(element);
        if (form_associated_element.is_candidate_for_constraint_validation() && !form_associated_element.satisfies_its_constraints())
            return true;

        return false;
    }
    case CSS::PseudoClass::Required: {
        // https://html.spec.whatwg.org/multipage/semantics-other.html#selector-required

        // The :required pseudo-class must match any element falling into one of the following categories:
        // - input elements that are required
        if (auto const* input_element = as_if<Web::HTML::HTMLInputElement>(element)) {
            if (input_element->required_applies() && input_element->has_attribute(HTML::AttributeNames::required))
                return true;
        }
        // - select elements that have a required attribute
        else if (auto const* select_element = as_if<Web::HTML::HTMLSelectElement>(element)) {
            if (select_element->has_attribute(HTML::AttributeNames::required))
                return true;
        }
        // - textarea elements that have a required attribute
        else if (auto const* textarea_element = as_if<Web::HTML::HTMLTextAreaElement>(element)) {
            if (textarea_element->has_attribute(HTML::AttributeNames::required))
                return true;
        }

        return false;
    }
    case CSS::PseudoClass::Optional: {
        // https://html.spec.whatwg.org/multipage/semantics-other.html#selector-optional

        // The :optional pseudo-class must match any element falling into one of the following categories:
        // - input elements to which the required attribute applies that are not required
        if (auto const* input_element = as_if<Web::HTML::HTMLInputElement>(element)) {
            if (input_element->required_applies() && !input_element->has_attribute(HTML::AttributeNames::required))
                return true;

            // AD-HOC: Chromium and Webkit also match for hidden inputs (and WPT expects this)
            // See: https://github.com/whatwg/html/issues/11273
            if (input_element->type_state() == HTML::HTMLInputElement::TypeAttributeState::Hidden)
                return true;
        }
        // - select elements that do not have a required attribute
        else if (auto const* select_element = as_if<Web::HTML::HTMLSelectElement>(element)) {
            if (!select_element->has_attribute(HTML::AttributeNames::required))
                return true;
        }
        // - textarea elements that do not have a required attribute
        else if (auto const* textarea_element = as_if<Web::HTML::HTMLTextAreaElement>(element)) {
            if (!textarea_element->has_attribute(HTML::AttributeNames::required))
                return true;
        }

        return false;
    }
    case CSS::PseudoClass::Default: {
        // https://html.spec.whatwg.org/multipage/semantics-other.html#selector-default

        // The :default pseudo-class must match any element falling into one of the following categories:
        if (auto const* form_associated_element = as_if<Web::HTML::FormAssociatedElement>(element)) {
            // - Submit buttons that are default buttons of their form owner.
            if (form_associated_element->is_submit_button() && form_associated_element->form() && form_associated_element->form()->default_button() == form_associated_element)
                return true;

            // - input elements to which the checked attribute applies and that have a checked attribute
            if (auto const* input_element = as_if<Web::HTML::HTMLInputElement>(form_associated_element)) {
                if (input_element->checked_applies() && input_element->has_attribute(HTML::AttributeNames::checked))
                    return true;
            }
        }
        // - option elements that have a selected attribute
        else if (auto const* option_element = as_if<Web::HTML::HTMLOptionElement>(element)) {
            if (option_element->has_attribute(HTML::AttributeNames::selected))
                return true;
        }

        return false;
    }
    case CSS::PseudoClass::State: {
        // https://html.spec.whatwg.org/multipage/semantics-other.html#selector-custom
        // The :state(identifier) pseudo-class must match all custom elements whose states set's set entries contains identifier.
        if (!element.is_custom())
            return false;
        if (auto* custom_state_set = element.custom_state_set())
            return custom_state_set->has_state(pseudo_class.ident->string_value);
        return false;
    }
    case CSS::PseudoClass::Heading: {
        // https://html.spec.whatwg.org/multipage/semantics-other.html#selector-heading
        // The :heading pseudo-class must match all h1, h2, h3, h4, h5, and h6 elements.

        // https://html.spec.whatwg.org/multipage/semantics-other.html#selector-heading-functional
        // The :heading(integer#) pseudo-class must match all h1, h2, h3, h4, h5, and h6 elements that have a heading level of integer. [CSSSYNTAX] [CSSVALUES]

        // NB: We combine the "is this an h* element?" and "what is it's level?" checks together here.
        if (!element.is_html_element())
            return false;
        auto heading_level = [](auto& local_name) -> Optional<int> {
            if (local_name == HTML::TagNames::h1)
                return 1;
            if (local_name == HTML::TagNames::h2)
                return 2;
            if (local_name == HTML::TagNames::h3)
                return 3;
            if (local_name == HTML::TagNames::h4)
                return 4;
            if (local_name == HTML::TagNames::h5)
                return 5;
            if (local_name == HTML::TagNames::h6)
                return 6;
            return {};
        }(element.lowercased_local_name());

        if (!heading_level.has_value())
            return false;

        if (pseudo_class.levels.is_empty())
            return true;

        return pseudo_class.levels.contains_slow(heading_level.value());
    }
    }

    return false;
}

static ALWAYS_INLINE bool matches_namespace(
    CSS::Selector::SimpleSelector::QualifiedName const& qualified_name,
    DOM::Element const& element,
    GC::Ptr<CSS::CSSStyleSheet const> style_sheet_for_rule)
{
    switch (qualified_name.namespace_type) {
    case CSS::Selector::SimpleSelector::QualifiedName::NamespaceType::Default:
        // "if no default namespace has been declared for selectors, this is equivalent to *|E."
        if (!style_sheet_for_rule || !style_sheet_for_rule->default_namespace_rule())
            return true;
        // "Otherwise it is equivalent to ns|E where ns is the default namespace."
        return element.namespace_uri() == style_sheet_for_rule->default_namespace_rule()->namespace_uri();
    case CSS::Selector::SimpleSelector::QualifiedName::NamespaceType::None:
        // "elements with name E without a namespace"
        return !element.namespace_uri().has_value();
    case CSS::Selector::SimpleSelector::QualifiedName::NamespaceType::Any:
        // "elements with name E in any namespace, including those without a namespace"
        return true;
    case CSS::Selector::SimpleSelector::QualifiedName::NamespaceType::Named:
        // "elements with name E in namespace ns"
        // Unrecognized namespace prefixes are invalid, so don't match.
        // (We can't detect this at parse time, since a namespace rule may be inserted later.)
        // So, if we don't have a context to look up namespaces from, we fail to match.
        if (!style_sheet_for_rule)
            return false;

        auto selector_namespace = style_sheet_for_rule->namespace_uri(qualified_name.namespace_);
        return selector_namespace.has_value() && selector_namespace.value() == element.namespace_uri();
    }
    VERIFY_NOT_REACHED();
}

static inline bool matches(CSS::Selector::SimpleSelector const& component, DOM::Element const& element, GC::Ptr<DOM::Element const> shadow_host, MatchContext& context, GC::Ptr<DOM::ParentNode const> scope, SelectorKind selector_kind, [[maybe_unused]] GC::Ptr<DOM::Element const> anchor)
{
    if (should_block_shadow_host_matching(component, shadow_host, element))
        return false;
    switch (component.type) {
    case CSS::Selector::SimpleSelector::Type::Universal:
    case CSS::Selector::SimpleSelector::Type::TagName: {
        auto const& qualified_name = component.qualified_name();

        // Reject if the tag name doesn't match
        if (component.type == CSS::Selector::SimpleSelector::Type::TagName) {
            // See https://html.spec.whatwg.org/multipage/semantics-other.html#case-sensitivity-of-selectors
            if (element.document().document_type() == DOM::Document::Type::HTML && element.namespace_uri() == Namespace::HTML) {
                if (qualified_name.name.lowercase_name != element.local_name())
                    return false;
            } else if (!qualified_name.name.name.equals_ignoring_ascii_case(element.local_name())) {
                return false;
            }
        }

        return matches_namespace(qualified_name, element, context.style_sheet_for_rule);
    }
    case CSS::Selector::SimpleSelector::Type::Id:
        return component.name() == element.id();
    case CSS::Selector::SimpleSelector::Type::Class: {
        // Class selectors are matched case insensitively in quirks mode.
        // See: https://drafts.csswg.org/selectors-4/#class-html
        auto case_sensitivity = element.document().in_quirks_mode() ? CaseSensitivity::CaseInsensitive : CaseSensitivity::CaseSensitive;
        return element.has_class(component.name(), case_sensitivity);
    }
    case CSS::Selector::SimpleSelector::Type::Attribute:
        return matches_attribute(component.attribute(), context.style_sheet_for_rule, element);
    case CSS::Selector::SimpleSelector::Type::PseudoClass:
        return matches_pseudo_class(component.pseudo_class(), element, shadow_host, context, scope, selector_kind);
    case CSS::Selector::SimpleSelector::Type::PseudoElement:
        if (component.pseudo_element().type() == CSS::PseudoElement::Slotted) {
            VERIFY(context.slotted_element);
            return matches(component.pseudo_element().compound_selector(), *context.slotted_element, shadow_host, context);
        }
        // Pseudo-element matching/not-matching is handled in the top level matches().
        return true;
    case CSS::Selector::SimpleSelector::Type::Nesting:
        // Nesting either behaves like :is(), or like :scope.
        // :is() is handled already, by us replacing it with :is() directly, so if we
        // got here, it's :scope.
        return matches_pseudo_class(CSS::Selector::SimpleSelector::PseudoClassSelector { .type = CSS::PseudoClass::Scope }, element, shadow_host, context, scope, selector_kind);
    case CSS::Selector::SimpleSelector::Type::Invalid:
        // Invalid selectors never match
        return false;
    }
    VERIFY_NOT_REACHED();
}

bool matches(CSS::Selector const& selector, int component_list_index, DOM::Element const& element, GC::Ptr<DOM::Element const> shadow_host, MatchContext& context, GC::Ptr<DOM::ParentNode const> scope, SelectorKind selector_kind, GC::Ptr<DOM::Element const> anchor)
{
    auto& compound_selector = selector.compound_selectors()[component_list_index];
    for (auto& simple_selector : compound_selector.simple_selectors) {
        if (!matches(simple_selector, element, shadow_host, context, scope, selector_kind, anchor)) {
            return false;
        }
    }

    if (selector_kind == SelectorKind::Relative && component_list_index == 0) {
        VERIFY(anchor);
        return &element != anchor;
    }

    switch (compound_selector.combinator) {
    case CSS::Selector::Combinator::None:
        VERIFY(selector_kind != SelectorKind::Relative);
        return true;
    case CSS::Selector::Combinator::Descendant:
        VERIFY(component_list_index != 0);
        for (auto ancestor = traverse_up(element, shadow_host); ancestor; ancestor = traverse_up(ancestor, shadow_host)) {
            if (!is<DOM::Element>(*ancestor))
                continue;
            if (ancestor == anchor)
                return false;
            if (matches(selector, component_list_index - 1, static_cast<DOM::Element const&>(*ancestor), shadow_host, context, scope, selector_kind, anchor))
                return true;
        }
        return false;
    case CSS::Selector::Combinator::ImmediateChild: {
        VERIFY(component_list_index != 0);
        auto parent = traverse_up(element, shadow_host);
        if (!parent || !parent->is_element())
            return false;
        return matches(selector, component_list_index - 1, static_cast<DOM::Element const&>(*parent), shadow_host, context, scope, selector_kind, anchor);
    }
    case CSS::Selector::Combinator::NextSibling:
        if (context.collect_per_element_selector_involvement_metadata) {
            const_cast<DOM::Element&>(element).set_affected_by_direct_sibling_combinator(true);
            auto new_sibling_invalidation_distance = max(selector.sibling_invalidation_distance(), element.sibling_invalidation_distance());
            const_cast<DOM::Element&>(element).set_sibling_invalidation_distance(new_sibling_invalidation_distance);
        }
        VERIFY(component_list_index != 0);
        if (auto* sibling = element.previous_element_sibling())
            return matches(selector, component_list_index - 1, *sibling, shadow_host, context, scope, selector_kind, anchor);
        return false;
    case CSS::Selector::Combinator::SubsequentSibling:
        if (context.collect_per_element_selector_involvement_metadata) {
            const_cast<DOM::Element&>(element).set_affected_by_indirect_sibling_combinator(true);
        }
        VERIFY(component_list_index != 0);
        for (auto* sibling = element.previous_element_sibling(); sibling; sibling = sibling->previous_element_sibling()) {
            if (matches(selector, component_list_index - 1, *sibling, shadow_host, context, scope, selector_kind, anchor))
                return true;
        }
        return false;
    case CSS::Selector::Combinator::Column:
        TODO();
    }
    VERIFY_NOT_REACHED();
}

bool fast_matches(CSS::Selector const& selector, DOM::Element const& element_to_match, GC::Ptr<DOM::Element const> shadow_host, MatchContext& context);

bool matches(CSS::Selector const& selector, DOM::Element const& element, GC::Ptr<DOM::Element const> shadow_host, MatchContext& context, Optional<CSS::PseudoElement> pseudo_element, GC::Ptr<DOM::ParentNode const> scope, SelectorKind selector_kind, GC::Ptr<DOM::Element const> anchor)
{
    if (selector_kind == SelectorKind::Normal && selector.can_use_fast_matches()) {
        return fast_matches(selector, element, shadow_host, context);
    }
    VERIFY(!selector.compound_selectors().is_empty());
    if (pseudo_element.has_value() && selector.pseudo_element().has_value() && selector.pseudo_element().value().type() != pseudo_element)
        return false;
    if (!pseudo_element.has_value() && selector.pseudo_element().has_value())
        return false;
    return matches(selector, selector.compound_selectors().size() - 1, element, shadow_host, context, scope, selector_kind, anchor);
}

static bool fast_matches_simple_selector(CSS::Selector::SimpleSelector const& simple_selector, DOM::Element const& element, GC::Ptr<DOM::Element const> shadow_host, MatchContext& context)
{
    if (should_block_shadow_host_matching(simple_selector, shadow_host, element))
        return false;

    switch (simple_selector.type) {
    case CSS::Selector::SimpleSelector::Type::Universal:
        return matches_namespace(simple_selector.qualified_name(), element, context.style_sheet_for_rule);
    case CSS::Selector::SimpleSelector::Type::TagName:
        // https://html.spec.whatwg.org/multipage/semantics-other.html#case-sensitivity-of-selectors
        // When comparing a CSS element type selector to the names of HTML elements in HTML documents, the CSS element type selector must first be converted to ASCII lowercase. The
        // same selector when compared to other elements must be compared according to its original case. In both cases, to match the values must be identical to each other (and therefore
        // the comparison is case sensitive).
        if (element.namespace_uri() == Namespace::HTML && element.document().document_type() == DOM::Document::Type::HTML) {
            if (simple_selector.qualified_name().name.lowercase_name != element.local_name())
                return false;
        } else if (simple_selector.qualified_name().name.name != element.local_name()) {
            // NOTE: Any other elements are either SVG, XHTML or MathML, all of which are case-sensitive.
            return false;
        }
        return matches_namespace(simple_selector.qualified_name(), element, context.style_sheet_for_rule);
    case CSS::Selector::SimpleSelector::Type::Class: {
        // Class selectors are matched case insensitively in quirks mode.
        // See: https://drafts.csswg.org/selectors-4/#class-html
        auto case_sensitivity = element.document().in_quirks_mode() ? CaseSensitivity::CaseInsensitive : CaseSensitivity::CaseSensitive;
        return element.has_class(simple_selector.name(), case_sensitivity);
    }
    case CSS::Selector::SimpleSelector::Type::Id:
        return simple_selector.name() == element.id();
    case CSS::Selector::SimpleSelector::Type::Attribute:
        return matches_attribute(simple_selector.attribute(), context.style_sheet_for_rule, element);
    case CSS::Selector::SimpleSelector::Type::PseudoClass:
        return matches_pseudo_class(simple_selector.pseudo_class(), element, shadow_host, context, nullptr, SelectorKind::Normal);
    default:
        VERIFY_NOT_REACHED();
    }
}

static bool fast_matches_compound_selector(CSS::Selector::CompoundSelector const& compound_selector, DOM::Element const& element, GC::Ptr<DOM::Element const> shadow_host, MatchContext& context)
{
    for (auto const& simple_selector : compound_selector.simple_selectors) {
        if (!fast_matches_simple_selector(simple_selector, element, shadow_host, context))
            return false;
    }
    return true;
}

bool fast_matches(CSS::Selector const& selector, DOM::Element const& element_to_match, GC::Ptr<DOM::Element const> shadow_host, MatchContext& context)
{
    DOM::Element const* current = &element_to_match;

    ssize_t compound_selector_index = selector.compound_selectors().size() - 1;

    if (!fast_matches_compound_selector(selector.compound_selectors().last(), *current, shadow_host, context))
        return false;

    // NOTE: If we fail after following a child combinator, we may need to backtrack
    //       to the last matched descendant. We store the state here.
    struct {
        GC::Ptr<DOM::Element const> element;
        ssize_t compound_selector_index = 0;
    } backtrack_state;

    for (;;) {
        // NOTE: There should always be a leftmost compound selector without combinator that kicks us out of this loop.
        VERIFY(compound_selector_index >= 0);

        auto const* compound_selector = &selector.compound_selectors()[compound_selector_index];

        switch (compound_selector->combinator) {
        case CSS::Selector::Combinator::None:
            return true;
        case CSS::Selector::Combinator::Descendant:
            backtrack_state = { current->parent_element(), compound_selector_index };
            compound_selector = &selector.compound_selectors()[--compound_selector_index];
            for (current = current->parent_element(); current; current = current->parent_element()) {
                if (fast_matches_compound_selector(*compound_selector, *current, shadow_host, context))
                    break;
            }
            if (!current)
                return false;
            break;
        case CSS::Selector::Combinator::ImmediateChild:
            compound_selector = &selector.compound_selectors()[--compound_selector_index];
            current = current->parent_element();
            if (!current)
                return false;
            if (!fast_matches_compound_selector(*compound_selector, *current, shadow_host, context)) {
                if (backtrack_state.element) {
                    current = backtrack_state.element;
                    compound_selector_index = backtrack_state.compound_selector_index;
                    continue;
                }
                return false;
            }
            break;
        default:
            VERIFY_NOT_REACHED();
        }
    }
}

}
